Skip to content

ci(deps): bump dawidd6/action-download-artifact from 25 to 27 - #53

Merged
eldonm merged 97 commits into
devfrom
dependabot/github_actions/dawidd6/action-download-artifact-27
Oct 11, 2026
Merged

eldonm merged 97 commits into
devfrom
dependabot/github_actions/dawidd6/action-download-artifact-27

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Oct 5, 2026

Copy link
Copy Markdown
Contributor

Bumps dawidd6/action-download-artifact from 25 to 27.

Release notes

Sourced from dawidd6/action-download-artifact's releases.

v27

What's Changed

Full Changelog: dawidd6/action-download-artifact@v26...v27

v26

Full Changelog: dawidd6/action-download-artifact@v25...v26

Commits
  • eab87c9 fix: merge unfiltered runs into filtered run search (#432)
  • 27e4ae6 feat: log skipped workflow runs in debug mode
  • See full diff in compare view

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

eldonm and others added 30 commits June 14, 2026 14:50
* feat(db): add PostgreSQL backend with serverless-tuned pooling

PostgresDB joins JsonDB/SQLite/MongoDB/DynamoDB as a first-class
backend. Targets Neon, Aurora Serverless v2, and self-hosted PG 15+.

- jvspatial/db/postgres.py — asyncpg pool with auto-tuned sizing
  (Lambda: 0/3, long-running: 2/10), session/transaction pooler modes,
  schema-scoped tables, JSONB storage, vector column support, native
  $near peeling, multi-tenant RLS via current_tenant contextvar,
  transactional save/get/find/delete via PostgresTransaction, and
  drop_deprecated_indexes for migrations.
- jvspatial/db/_postgres_translate.py — Mongo-style query → SQL
  translator with positional-placeholder builder.
- jvspatial/db/factory.py — register "postgres"/"postgresql" with
  env-driven defaults (DSN, pool sizes, pooler mode).
- jvspatial/db/transaction.py — backend-agnostic transaction_context.
- docker/ — local dev pgvector container + .env example.
- docs/md/postgres-guide.md, neon-deployment.md,
  aurora-serverless-deployment.md, multi-tenant-rls.md,
  cursor-pagination.md, schema-migrations.md, vector-store.md.
- tests/db/test_postgres_{unit,integration,translate}.py,
  tests/db/test_cursor_pagination.py.

Companion changes pulled in:
- jvspatial/core/migrations.py + tests — record-level schema migrations.
- jvspatial/core/validate.py + tests — graph-shape validation.
- jvspatial/core/entities/walker_components/trail_store.py + tests —
  pluggable walker trail persistence.
- jvspatial/api/auth/_session_store.py + tests — pluggable auth session
  store (in-process / Redis) for multi-worker deployments.
- jvspatial/observability/tracing.py + tests — OTel span helpers.
- jvspatial/cli.py — `jvspatial migrate` CLI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(spec): reusable OAuth 2.1 service design (M1)

Authlib-based OAuth 2.1 AS+RS layered on jvspatial's existing auth.
RS256+JWKS access tokens, DCR, PRM/AS metadata, audience binding.
Foundation for OAuth-secured MCP (M2 = Integral MCP endpoint, M3 = UI).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(plan): M1a OAuth foundation implementation plan

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* build(oauth): add cryptography dependency for RS256

* feat(oauth): OAuthClient + AuthorizationCode storage models

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(oauth): RS256 signing-key store + JWKS

Add OAuthSigningKey Object model and keys.py module: RSA-2048
keypair generation, persist/load via Object.find, idempotent
ensure_signing_key(), and JWKS builder (public keys only). All
three TDD tests pass. PyJWT 2.x to_jwk requires cryptography key
object; PEM loaded via serialization.load_pem_public_key before
conversion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(oauth): AuthConfig OAuth fields (off by default)

Add oauth_enabled, oauth_issuer_url, oauth_prefix, oauth_supported_scopes,
oauth_dcr_enabled, oauth_access_token_ttl_minutes, oauth_code_ttl_seconds,
and accept_oauth_bearer to AuthConfig. All default off/empty so existing
apps are unaffected. Covered by two-test TDD suite in
tests/api/auth/oauth/test_oauth_authconfig.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(spec): M1b design addendum — Authlib AS integration + jvspatial wiring map

* docs(plan): M1b-1 OAuth core authorization server plan

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* build(oauth): add authlib dependency

* feat(oauth): anyio sync/async bridge + Starlette OAuth2Request wrapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(oauth): ClientMixin adapter over OAuthClient

Implements OAuthClientAdapter wrapping OAuthClient so Authlib can
validate redirect URIs, grant/response types, scope filtering, and
token endpoint auth methods (public PKCE + confidential secret check).
All three tests (redirect/grant checks, public auth method + scope
filter, confidential secret check) pass; black/isort/flake8/mypy clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(oauth): authorization-code+PKCE server issuing RS256 tokens (anyio-bridged)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(oauth): enforce mandatory S256 PKCE for all clients (security)

Adds RequiredS256CodeChallenge subclass that overrides the authorize-step
hook to reject missing code_challenge (regardless of client auth method)
and reject code_challenge_method=plain. Registers it in
build_authorization_server instead of CodeChallenge(required=True), which
only enforced PKCE for public clients (auth_method=="none").

Two regression tests added: confidential client without PKCE is rejected,
and plain code_challenge_method is rejected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(plan): M1b-2 refresh grant + scope-intersection + token hardening

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(oauth): OAuthRefreshToken model + refresh store

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(oauth): issue + persist refresh tokens on auth-code flow

Wire _generate_refresh_token into JvSpatialJWTTokenGenerator so Authlib
emits an opaque rt_ token when client.check_grant_type("refresh_token")
is true; implement save_token to persist the plaintext (hashed) via
refresh_store.mint_refresh_token through a positional async shim
(_persist_refresh) compatible with call_async's positional-only contract.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(oauth): refresh-token grant with rotation + revocation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(oauth): scope-permission intersection + nbf claim + atomic single-use code

Three hardening fixes from the M1b-2 spec §6:

1. scope∩permissions — `save_authorization_code` now reads `permissions`
   from `request.user` (when the grant_user dict carries it) and stores
   only the intersection of the client-allowed scope and the user's
   permissions in `AuthorizationCode.scope`.  Back-compat: callers that
   pass `{"id":...}` without `permissions` keep the full client scope.
   Because `create_token_response` reads scope from
   `authorization_code.get_scope()`, the intersected scope propagates
   to the token `scope` claim automatically with no generator changes.

2. nbf claim — `JvSpatialJWTTokenGenerator.get_extra_claims` overrides
   the Authlib base (which returns `{}`) to inject `"nbf": int(time.time())`.
   `access_token_generator` merges extra claims via `token_data.update(...)`,
   so `nbf` lands in every issued JWT.  super() is called first so any
   future upstream additions are preserved.

3. atomic single-use — `query_authorization_code` calls `_consume_code`
   (sets `consumed=True` + saves) *before* returning the `StoredAuthCode`
   to the grant.  A concurrent duplicate exchange sees `consumed=True` on
   the first load and is rejected.  `delete_authorization_code` remains
   idempotent (Authlib still calls it after token issuance; the record is
   already consumed at that point, which is harmless).

All 9 server-flow tests pass (incl. 2 new: scope-intersection+nbf,
back-compat no-permissions); all 24 OAuth tests pass.

* fix(oauth): consume auth code only after full validation (no retry lockout)

Move single-use enforcement from query_authorization_code (runs before
redirect_uri + PKCE validation) to delete_authorization_code (runs only
after all validation passes on the success path, per authlib 1.7.2
authorization_code.py:create_token_response). A request with a wrong
code_verifier or wrong redirect_uri previously burned the code, locking
out the legitimate client's retry. Now query_authorization_code is
read-only; delete_authorization_code is the single-use point.

Also corrects the atomicity docstring: single-use is serialized by the
single-process anyio bridge, not a DB-level CAS; multi-worker deployment
will require a conditional-update primitive (tracked for a later phase).

Adds regression test test_failed_verifier_does_not_burn_code_for_retry
proving a failed verifier does NOT lock out a correct retry, and confirms
the existing single-use replay test still rejects a replayed code.

* docs(plan): M1b-3a DCR + revocation + reuse-detection + metadata

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(oauth): refresh-token reuse detection (family revocation)

Implements OAuth 2.1 §rotation family kill (review finding I-3).
When a rotated/revoked refresh token is replayed, the entire rotation
family is immediately revoked (including the live child token), and the
request is rejected. Normal rotation is unaffected.

- OAuthRefreshToken: +family_id field (shared across rotations of one grant)
- refresh_store: mint_refresh_token gains family_id param; add find_any
  (lookup without is_active filter for replay detection) and revoke_family
  (kill all tokens in a family)
- server.py: authenticate_refresh_token stashes rec.family_id on
  request._oauth_family_id; save_token forwards it to _persist_refresh so
  the rotated token inherits the family; fresh uuid4 family_id generated at
  first mint (auth-code path). Replay of revoked token triggers revoke_family.

* feat(oauth): dynamic client registration (RFC 7591)

Implements JvSpatialClientRegistrationEndpoint (open registration,
MCP zero-config) + async_register_client on the authorization server.
JSON body wired via BasicOAuth2Payload on request.payload before
dispatching; public clients (auth_method=none) receive no client_secret
in the 201 response and store no secret hash.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(oauth): token revocation endpoint (RFC 7009)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(oauth): RFC 8414 AS metadata builder

Pure builder function ``build_as_metadata`` constructs the RFC 8414
Authorization Server metadata document (issuer, endpoints, PKCE,
grant types). Validated against ``authlib.oauth2.rfc8414.AuthorizationServerMetadata``
in the accompanying TDD test. Route to serve the document at
``/.well-known/oauth-authorization-server`` is deferred to M1b-3b.

* fix(oauth): DCR requires https redirect URIs (loopback http allowed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(plan): M1b-3b OAuth HTTP wiring (routes + mount + consent + rate-limit)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(oauth): mount OAuth routers + well-known metadata/jwks + startup key hook

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: OAuth program resume handoff (M1b-3b Task 1 done; next steps + known metadata-prefix bug)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(oauth): metadata advertises api-prefixed endpoints; DCR-over-HTTP test

build_as_metadata gains api_prefix kwarg (default "" for back-compat); the
well-known handler now passes APIRoutes.PREFIX so token/registration/revocation/
authorization endpoints are advertised as https://as.example/api/oauth/…
instead of missing the /api segment.  register route pre-reads JSON body then
builds a StarletteOAuth2Request with empty form to avoid double-read error.
New test_oauth_http_flow.py covers both assertions over TestClient.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(oauth): /authorize consent + session-user permission resolution

Implement the real GET/POST /oauth/authorize flow, replacing the HTML
placeholder. GET renders a consent page (client name + requested scopes +
approve/deny form re-carrying the OAuth params); POST builds grant_user from
the bearer-authenticated session user ONLY — its id plus effective permissions
via get_effective_permissions(roles, permissions, role_permission_mapping) —
and calls async_create_authorization_response so the authorization server
intersects scope with those permissions. Deny redirects with
error=access_denied. Permissions are never read from client/request input,
making the scope∩permissions narrowing the trust boundary.

- Thread get_current_user from the auth configurator into build_oauth_routers
  and Depends() it on both authorize routes (401 without bearer).
- Add JvSpatialAuthorizationServer.async_get_consent_grant to validate the
  authorize request off-thread (anyio bridge) and surface the client/scope for
  rendering; invalid requests yield a 400 page (no open redirect).
- Optional auth_config.oauth_consent_handler override; 503 when no
  get_current_user is wired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(oauth): rate-limit dynamic client registration (open-DCR abuse, I-1)

Add `oauth_dcr_rate_limit_per_minute` knob (default 12) to AuthConfig and
wire a tight rate-limit override onto `/api/oauth/register` at OAuth-configure
time — before `get_app()` builds the rate-limit middleware — so the MemoryRateLimitBackend
caps DCR bursts with a 429. When the server operator has not enabled rate limiting
globally, the flag is implicitly set to True so the override takes effect.
Existing auth and OAuth tests (42) remain green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(oauth): /authorize deny path validates redirect_uri (no open redirect)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(plan): M1c resource server (JWKS verifier + PRM + accept_oauth_bearer)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(oauth): resource-server access-token verifier

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(oauth): protected-resource metadata (RFC 9728) + 401 helper

Adds `build_prm()` and `www_authenticate_header()` to metadata.py.
Mounts `/.well-known/oauth-protected-resource` on `well_known_router`
(active only when `oauth_enabled=True`). Exempts the PRM path from the
bearer-auth middleware unconditionally so FastAPI returns a natural 404
when OAuth is disabled, matching the existing AS-metadata + JWKS pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(oauth): accept OAuth bearer tokens on protected endpoints (accept_oauth_bearer)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(oauth): emit WWW-Authenticate resource_metadata on 401 (RFC 9728 discovery)

When `accept_oauth_bearer=True`, the middleware now attaches
`WWW-Authenticate: Bearer resource_metadata="<issuer>/.well-known/oauth-protected-resource"`
to every 401 response (missing bearer AND failed-auth paths), so MCP
clients can auto-discover the Authorization Server per RFC 9728 §5.1 /
RFC 6750 §3.  Header is absent when `accept_oauth_bearer=False` (no
change for non-OAuth servers).  Import is lazy/guarded to avoid
coupling when the oauth subpackage is unavailable.

* docs: OAuth handoff — M1 complete (AS+RS), next = merge + M2/M3

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(oauth): redirect unauthenticated GET /authorize to configured login SPA

When AuthConfig.oauth_authorize_login_redirect is set, an unauthenticated
GET /oauth/authorize now 302-redirects the browser to that SPA login URL
with the original OAuth query string appended, instead of returning the
legacy 401 JSON. This makes the OAuth authorization-code browser flow
completable. Empty (default) preserves the legacy 401 behavior.

The GET handler uses a non-raising optional-user seam (_optional_current_user)
that invokes get_current_user imperatively and converts its 401 to None, so
it can branch on the unauthenticated case. POST /authorize and the authed-GET
consent page are unchanged (still bearer-required). The redirect target is
ONLY the trusted config base + the original query — never a request-derived
host — so it cannot be turned into an open redirect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(oauth): supported-scope ceiling + empty-perms/wildcard scope hardening

Make the authorization-code scope grant secure-by-default once a scope
ceiling is declared, while staying a strict no-op for callers that do
not declare one.

- build_authorization_server / JvSpatialAuthorizationServer gain an
  optional supported_scopes; stored as self._supported_scopes (defaults
  to []). routes.py wires auth_config.oauth_supported_scopes through.
- save_authorization_code intersects the permission-narrowed scope
  against the supported set when non-empty (empty => skip, back-compat).
- _intersect_scope: permissions is None => no narrowing (absent key,
  back-compat); '*' in permissions => authorize all requested scope
  (admin wildcard, fixes the inverted-admin footgun); else membership
  filter. A present-but-empty [] now narrows to nothing.
- save_authorization_code no longer collapses []->None, so an explicit
  empty permission set denies all scope.

BEHAVIOUR CHANGE (for CHANGELOG in M4-T6): a present-but-empty
request.user['permissions'] == [] now grants NO scope (was: full
requested scope). Absent key still preserves full scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(oauth): DCR filters requested scope against supported scopes

When the authorization server declares a supported-scope ceiling
(server._supported_scopes non-empty), the DCR endpoint now silently
intersects a client's requested scope against that set in
extract_client_metadata, so a client cannot self-register an unsupported
or elevated scope. Unsupported tokens are dropped (RFC 7591 permits the
AS to filter) rather than rejected, keeping zero-config MCP registration
working. Filtering in extract_client_metadata (not save_client) keeps a
single source of truth so both the persisted record and the echoed 201
scope reflect the filtered value.

Back-compat: an empty/undeclared ceiling is a no-op — requested scope is
persisted verbatim. This is defense-in-depth; the authorize-time ceiling
in JvSpatialAuthCodeGrant.save_authorization_code (M4-T1) remains the
primary bound on issued-token scope.

scopes_supported is deliberately NOT advertised in get_server_metadata:
Authlib's get_claims_options turns it into a hard issuperset validation
that would 400 an out-of-set scope before our silent filter runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(oauth): atomic auth-code consume (CAS) to close multi-worker TOCTOU

Replace the blind `consumed = True; save()` in _consume_code with an
atomic compare-and-swap (find_one_and_update predicated on consumed=False),
mirroring the work_claim.py idiom. On a lost race (None result) the consume
raises InvalidGrantError — the same error a replayed/consumed code produces
via query_authorization_code — so the loser never gets a second token.

Consume still runs after PKCE/redirect validation (failed verifier does
not burn the code). Atomic on postgres (SELECT ... FOR UPDATE) and mongo
(native findOneAndUpdate); best-effort on sqlite/json (no regression vs
the prior blind write). Drop the consumed record from the GraphContext
cache so a stale cached entry can't let a replay slip through.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ratelimit): IP-only identifier + first-class pluggable backend

7a — Drop the attacker-controlled User-Agent from the unauthenticated
rate-limit bucket key. The key is now the source IP alone (sha256-hashed):
folding in the UA let an attacker rotate the header per request to mint a
fresh bucket and evade the per-IP cap. Authenticated `user:{id}` path is
unchanged. (X-Forwarded-For trusted-proxy parsing remains deferred as 7b;
`request.client.host` stays as-is, documented as a known proxy limitation.)

3 — Declare `rate_limit_backend` as a real Optional[RateLimitBackend] field
on ServerConfig (default None -> process-local MemoryRateLimitBackend, behaviour
unchanged), replacing the prior getattr lookup. ServerConfig now allows
arbitrary types so the Protocol-typed backend can be stored. Documents the
multi-worker limitation (in-memory counter is per-process, so effective cap is
N x configured under N workers — including the DCR /oauth/register cap) on the
field, the server_configurator selection site, and _wire_dcr_rate_limit.

Tests: UA-independence of the identifier + same-IP shared bucket (no UA-rotation
evasion); rate_limit_backend field declared/defaults-None/accepts-instance and
server_configurator consumes a supplied backend (falls back to Memory otherwise).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(oauth): jti denylist for access-token revocation (RFC 7009)

Access tokens are stateless RS256 JWTs that previously stayed valid until
their exp regardless of /oauth/revoke. Add a per-token jti denylist so a
holder can revoke a specific access token before expiry:

- OAuthRevokedToken Object (jti + self-expiring expires_at = token exp)
- denylist.py store: revoke_jti (idempotent) / is_jti_revoked
- resource.verify_oauth_access_token: require jti + reject denylisted jtis
  (Authlib's RFC-9068 generator always stamps jti; verified by decoding a
  freshly-minted token). One indexed lookup per RS validation (hot-path note;
  in-proc cache is a future optimization).
- revocation endpoint: when an access-token JWT is presented (or
  token_type_hint=access_token), validate it via the same verify path and
  denylist its jti. Refresh-token revocation unchanged. Still requires
  client-auth + token possession (check_client on the client_id claim), so a
  caller can only revoke a token they hold.

Per-token only; mass revocation (per-(user,client) revoked-after watermark)
is a separate future item since stateless jtis can't be enumerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(oauth): M4 hardening completion record + holistic security review

Records the five M4 fixes (scope ceiling, DCR scope filter, auth-code CAS,
IP-only rate-limit key + pluggable backend, jti denylist), the two intentional
behavior changes (permissions:[] => deny scope; IP-only rate-limit key), the
holistic adversarial review verdict (no over-grant on any branch, no fail-open,
back-compat preserved, no cross-cutting bypass), and the two documented
deferrals (Item 5 RFC-8707 per-request resource->aud; Item 7b trusted-XFF).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(oauth): M4 record — cross-repo verification outcome (integral consent ceiling fix 1bb5f5a)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: keep jvspatial implementation-agnostic — generalize integral references

jvspatial is a generic application framework; remove the two remaining
authoritative references to a specific downstream app (integral):
- db/postgres.py: partial-filter docstring now says 'jvspatial and its
  downstream applications' instead of naming jvagent/integral
- docs/md/multi-tenant-rls.md: RLS is 'a load-bearing isolation primitive
  for any multi-tenant platform built on it', not 'the Integral platform'

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(oauth): use generic app.example.com fixtures (no app-specific names in jvspatial)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(oauth): add M4 hardening implementation plan to .planning (companion to the COMPLETE record)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: enable pretty printing for JSON serialization across both orjson and standard json backends

* chore(release): clean up packaging for 0.0.9 PyPI release

Make pyproject.toml the single source of build metadata and fix the
metadata/packaging issues found in the pre-release review.

Packaging:
- Remove setup.py; resolve version dynamically via
  [tool.setuptools.dynamic] from jvspatial/version.py. Fixes a
  dual-source conflict where the wheel published Requires-Python
  >=3.8 (EOL) while the project targets >=3.9.
- Add the `cache` extra (redis[hiredis]) so `pip install
  jvspatial[cache]` works; it backs jvspatial.cache.redis.
- Complete the `all` extra: lambda, postgres, pgvector, otel, cache,
  scheduler.
- Remove the redundant top-level requirements*.txt files; deps live
  solely in pyproject.toml extras.

CI / release:
- publish.yml: switch from API-token upload to PyPI Trusted
  Publishing (OIDC) via pypa/gh-action-pypi-publish; add id-token:
  write; drop dead setup.py/requirements config-change grep.
- security.yml: drop the now-inert requirements*.txt path filters.

Docs / templates:
- CHANGELOG: promote [Unreleased] to [0.0.9] - 2026-06-14 with a
  Packaging section.
- bug_report.md: drop downstream Jaclang/Jivas fields; add OS, DB
  backend, serverless mode.
- Update workflow docs (README/VERSIONING/WORKFLOW_SUMMARY),
  RELEASING.md, and docs/md to drop setup.py/requirements*.txt
  references.

Verified: clean sdist+wheel build, twine check PASSED, fresh-venv
install + import + CLI all green; wheel METADATA shows version 0.0.9
and Requires-Python >=3.9.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(security): stop unpickling untrusted Redis blobs in json mode; fix auth docs

Security:
- jvspatial/cache/redis.py: in the default `json` serialization mode the
  deserializer fell through to pickle.loads on any unprefixed value, so
  anyone able to write to the Redis keyspace could achieve RCE even with
  the "safe" default. JSON mode now refuses non-JSON values (raised and
  caught by get() as a cache miss -> recompute). Legacy pickle entries are
  readable only via explicit opt-in (allow_legacy_pickle=True /
  JVSPATIAL_REDIS_ALLOW_LEGACY_PICKLE=true) on a trusted keyspace.
  Explicit `pickle` mode is unchanged.

Docs:
- README + docs/md/{authentication,api-keys,migration}.md used a flat
  `auth_enabled=True` kwarg that ServerConfig silently ignores (auth
  stayed disabled). Corrected to the nested `auth=dict(...)` form.

Cleanup:
- jvspatial/cache/layered.py: replace print() Redis-unavailable warnings
  with logging.warning.
- .env.example: document JVSPATIAL_REDIS_ALLOW_LEGACY_PICKLE.
- tests: cover safe-default rejection, opt-in legacy read, and env toggle.

CHANGELOG: 0.0.9 Security/Fixed entries.

* fix(api): restore route introspection under Starlette >=0.52; fix CI

Two independent CI failures on the 0.0.9 PR.

1. Starlette >=0.52 broke auth/OpenAPI route introspection (4 failures).
   Newer Starlette wraps `app.include_router(...)` in an `_IncludedRouter`
   object instead of flattening the included APIRoutes into `app.routes`.
   jvspatial's auth resolver only scanned top-level APIRoute instances, so
   included routes (e.g. the file-storage GET/DELETE endpoints) looked
   unregistered -> deny-by-default -> spurious 401s; OpenAPI security
   padlocks were also dropped. Added `jvspatial/api/_route_utils.py`
   (`iter_api_routes`) which recurses through `_IncludedRouter`
   (`.original_router.routes`) and mounts, and routed every route-table
   scan through it: endpoint auth resolver (3 sites), OpenAPI security
   wiring, dynamic-walker registration, and duplicate-route detection.
   Compatible with old and new Starlette. Reproduced and verified fixed
   in a python:3.12-slim container (fastapi 0.137 / starlette 0.52);
   does not regress the older local stack (fastapi 0.119 / starlette 0.48).

2. Postgres unit tests errored when asyncpg absent (14 failures).
   `tests/db/test_postgres_unit.py` now `importorskip("asyncpg")` so it
   skips cleanly instead of erroring; CI installs the postgres+pgvector
   extras so the unit tests actually run (translator pushdown, pool
   auto-tuning, vector encoding). Live-DB integration tests still skip
   without JVSPATIAL_POSTGRES_TEST_DSN.

CHANGELOG: 0.0.9 Fixed entry for the Starlette compatibility fix.

* fix(deps): lift Starlette cap to resolve PYSEC-2026-161

Starlette 0.52.x carries PYSEC-2026-161 (fixed in 1.0.1); the previous
`starlette<1.0.0` cap pinned installs to the vulnerable line and failed
the pip-audit CI gate. The cap's original rationale (Starlette 1.0 drops
Router on_startup/on_shutdown) does not apply: jvspatial wires lifecycle
hooks through its own LifecycleManager (FastAPI lifespan), and route
introspection is version-agnostic via _route_utils.iter_api_routes.

Dependency is now `starlette>=0.46.0` (matches FastAPI's own floor, no
upper cap). Fresh installs resolve to a patched Starlette (verified 1.3.1).
Full suite green against Starlette 1.3.1; pip-audit reports no
vulnerabilities with the CI-exact install.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Tharick Jairam <tharick.jairam@v75inc.com>
Adds request-scoped identity map, single-hop find_connected_nodes,
auth service reuse, walker batching knobs, and observable db_op coverage.
Prepares integral perf audit substrate dependencies.

Co-authored-by: Cursor <cursoragent@cursor.com>
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 6 to 21.
- [Release notes](https://github.com/dawidd6/action-download-artifact/releases)
- [Commits](dawidd6/action-download-artifact@v6...v21)

---
updated-dependencies:
- dependency-name: dawidd6/action-download-artifact
  dependency-version: '21'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](actions/download-artifact@v5...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [peter-evans/create-or-update-comment](https://github.com/peter-evans/create-or-update-comment) from 4 to 5.
- [Release notes](https://github.com/peter-evans/create-or-update-comment/releases)
- [Commits](peter-evans/create-or-update-comment@v4...v5)

---
updated-dependencies:
- dependency-name: peter-evans/create-or-update-comment
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
release: 0.0.10 — graph perf, identity map, postgres joins
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
The 0.0.10 single-hop find_connected_nodes fast path had two regressions,
both invisible to the suite because JsonDB exposes no find_connected_nodes
(tests fell back to the slow edge scan):

1. Limit-before-filter: `limit` was pushed to the DB scan before the Python
   node-type filter, so node(node=T) / nodes(node=T, limit=n) could return
   nothing when a non-matching neighbor sorted first. Push `limit` to the DB
   only when there is no node filter; otherwise apply it after filtering.

2. Subtype resolution under an entity-name collision: rows were deserialized
   with the base Node class, so when two Node subclasses in one database share
   an entity name (e.g. an app User and an embedded-agent User),
   find_subclass_by_name returned the first global match and isinstance-based
   filtering dropped the valid neighbor. Deserialize using the caller's
   requested concrete type as the resolution hint.

Adds tests/core/test_connected_nodes_fast_path.py, which installs a
find_connected_nodes shim so the fast path is actually exercised; both tests
fail on 0.0.10 and pass here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 0.0.10 single-hop find_connected_nodes fast path had two regressions,
both invisible to the suite because JsonDB exposes no find_connected_nodes
(tests fell back to the slow edge scan):

1. Limit-before-filter: `limit` was pushed to the DB scan before the Python
   node-type filter, so node(node=T) / nodes(node=T, limit=n) could return
   nothing when a non-matching neighbor sorted first. Push `limit` to the DB
   only when there is no node filter; otherwise apply it after filtering.

2. Subtype resolution under an entity-name collision: rows were deserialized
   with the base Node class, so when two Node subclasses in one database share
   an entity name (e.g. an app User and an embedded-agent User),
   find_subclass_by_name returned the first global match and isinstance-based
   filtering dropped the valid neighbor. Deserialize using the caller's
   requested concrete type as the resolution hint.

Adds tests/core/test_connected_nodes_fast_path.py, which installs a
find_connected_nodes shim so the fast path is actually exercised; both tests
fail on 0.0.10 and pass here.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…paths

_translate_field_clause built (data #>> '{entity}') for every field
including entity/id/tenant_id, even though these are real top-level
Postgres columns (see _bootstrap_collection DDL + postgres.py's own
_pg_field_extract, which already special-cases them correctly for a
different code path). Every typed <NodeClass>.find(...) auto-injects
an entity discriminator filter, so this blinded every find() call to
the entity btree index and forced a full table scan on the shared
node table.

Reproduced against a live Postgres instance: entity = $1 seq-scanned
at ~298ms on a 2.4k-row table; after this fix it's an Index Scan on
node_entity_idx at ~0.03ms.
…lowlist

Both are real vars consumed directly in database_configurator.py, but
were missing from ALLOWED_ENV_KEYS — every server boot logged "Unknown
JVSPATIAL_* env var ... ignored" even though the var worked correctly.
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
…ns/checkout-7

ci(deps): bump actions/checkout from 6 to 7
…olumn-index

fix(postgres): translate entity/id/tenant_id to real columns; register observability env keys
The publish-on-push workflow re-uploaded the unchanged 0.0.11 on every merge
to main (incl. #26/#27), failing with `400 File already exists`. Two fixes:

- Bump __version__ 0.0.11 -> 0.0.12 to release the postgres translator
  column-index fix + observability env keys.
- Add `skip-existing: true` to the pypa/gh-action-pypi-publish step so future
  merges that don't bump the version are a no-op success instead of a red X.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
chore(release): bump to 0.0.12; skip-existing on PyPI publish
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](actions/setup-python@v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
…st noop

create_task(..., strict=True) promised that scheduling failures surface to the
caller, but strict only guarded one case: the LoggingNoop scheduler resolving
in serverless mode. The AWS schedulers themselves never raised —
AwsLambdaDeferredTaskScheduler swallowed invoke exceptions and returned a
synthetic reference, returned early (warn only) when AWS_LAMBDA_FUNCTION_NAME
was unset, and AwsSqsTaskScheduler silently no-op'd when unconfigured.

A caller passing strict=True is stating that it has failure handling of its
own — a 5xx to the webhook origin, a dedup claim to release — and that a
silently-dropped task is data loss. Handing that caller a reference for a task
that was never dispatched converts its retry path into exactly that loss:
jvagent's WhatsApp deferred-interact webhook returned 200 on real dispatch
failures, the wamid stayed claimed, and Meta's retry was dedup-blocked.

`strict: bool = False` is added to TaskScheduler.schedule and threaded through
dispatch_deferred_task. Under strict: aws_lambda raises on a missing function
name and re-raises invoke failures (an EventBridge failure still falls back to
direct invoke — a working fallback is not a dispatch failure); aws_sqs raises
when unconfigured (send_message failures already propagated); the noop stub
raises. Non-strict behaviour is byte-for-byte unchanged, so every existing
fire-and-forget caller keeps its semantics.

Tests cover both directions per path: strict raises where non-strict returns a
synthetic ref, and the success path still returns a reference.

2014 passed, 129 skipped; pre-commit clean on touched files.
`_find_sort_key` looked up sort fields with a flat `record.get(field)`, so a
spec like `sort=[("context.started_at", -1)]` produced `None` for every row
and left results in arbitrary order.

The SQLite and Postgres sort pushdowns (`translate_sort`) and Mongo's native
sort already resolve dotted paths, so the same query ordered correctly on
those backends and silently did not on JsonDB/DynamoDB — or on SQLite and
Postgres whenever the query fell back to the in-memory path.

Resolve dotted paths in `_find_sort_key`; a non-dict segment along the path
yields `None` rather than raising.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nding

`finalize_find_results` sorts descending via `reverse=True`, which flipped
`_find_sort_key`'s `(value is None, value)` flag along with the values and
floated records missing the sort field to the front.

Both SQL translators emit NULLS LAST for descending — SQLite via a leading
`(col IS NULL)` term that is itself sorted ASC, Postgres via an explicit
`DESC NULLS LAST` — and Mongo sorts missing values last. A "newest N"
`sort` + `limit` fetch therefore returned real rows on SQLite/Postgres/Mongo
and a window of records missing the field on the in-memory path.

Invert the None flag for descending sorts so missing values land last in both
directions. All missing values share a flag, so None is never compared against
a real value.

Also correct the comment in `_sqlite_translate.translate_sort`, which asserted
the in-memory sort already put NULLs last.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records the two properties every adapter has to produce identically whether it
pushes the sort into the backend or falls back to `finalize_find_results`:
dotted paths resolve into nested documents, and missing values sort last in
both directions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`translate_sort` returns None for a field path it cannot safely interpolate
(e.g. `context.my-field`), so the ordering has to happen in
`finalize_find_results`. The LIMIT was still pushed into the SQL, so the
database returned an arbitrary N rows and the in-memory sort ordered that
arbitrary subset: `find(sort=..., limit=10)` returned the top 10 of an
arbitrary 10 rather than the true top 10.

Withhold the LIMIT whenever the sort falls back to memory, then apply it
after sorting. `SQLiteDB.find` and `DynamoDB.find` already did this.

Also stop re-sorting vector (`$near`) results by the user's `sort`. The
comment says the vector ORDER BY wins when both are present, but forcing
`sort_sql = None` for vector queries meant the in-memory branch fired and
discarded the distance ordering.

Applies to both `PostgresDB.find` and the verbatim copy in
`PostgresTransaction.find`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…value tail

Two defects in `GraphContext.find_page` keyset pagination.

The cursor payload was minted with a flat `last.get(primary_field)`. For a
dotted sort field the value is nested, so every cursor encoded `sort: None`
and the next page compared against it — raising `TypeError: '>' not supported
between instances of 'int' and 'NoneType'` out of `QueryEngine` on JsonDB
rather than paging. Mint the cursor with `resolve_sort_value`, the same path
walk the adapters and `finalize_find_results` use.

Records missing the sort field sort last in both directions, but the keyset
filter `{field: {"$lt": value}}` can never match a record that has no value,
so iteration stopped at the last record that had one and the trailing run was
unreachable. Add a `{field: None}` branch — which matches both an explicit
null and a missing key — and, when the cursor itself was minted inside that
run, walk it by `id` alone.

Extract the path walk from `_find_sort_key` into `resolve_sort_value` and
export it so cursor minting and adapter sorting cannot drift apart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ObjectPager.get_page` fetched a slice with a DB-side `sort` + `limit`, then
re-sorted that page in Python with
`item.get("context", {}).get(order_by, 0)`. A record missing `order_by`
became `0` and sorted among the real values, while the slice that produced
the page had already placed it in the trailing missing-value run. The two
orderings disagree, so a record could appear on two pages or on none.

The blanket `contextlib.suppress(KeyError, TypeError)` around it also left a
page silently in DB order whenever the key raised.

Route the safety net through `finalize_find_results` instead. It is now a
genuine no-op when the backend honored the sort, and matches the nulls-last
contract when it did not.

`test_get_page_with_ordering` was passing on the in-Python re-sort alone —
its mock ignored `sort` and `limit` entirely, so it could not have caught
this. Switch it to `mock_find_respecting_limit`, which the keyset tests in
the same file already use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ract

Three docstrings still said "NULLs sort last for ascending, first for
descending, mirroring finalize_find_results" — the opposite of what the code
emits and of the contract: the module docstring and `translate_sort` docstring
in `_sqlite_translate.py`, and `translate_sort` in `_postgres_translate.py`.
Only the inline comment was corrected when the in-memory ordering was fixed.

Move the `find` sort contract from SPEC §4.2 (capability flags) to §4.1,
beside the `Database` method table it governs, and extend it with the rule
that `limit` must not be pushed down when the sort is not, plus a Known
divergences table: MongoDB's native `cursor.sort()` places missing values
first on ascending sorts (documented, not normalized — compensating needs an
aggregation pipeline on every find), array-index path segments, and
heterogeneous value types.

Document the contract on `Database.find` for adapter authors, and correct
`Database.find_iter`, which claimed a composite `(sort_value, id)` cursor it
does not implement — it tracks `id` only, so a non-`id` sort drops records
that sort late but carry a lower `id`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`test_find_sort_nulls_last.py` only string-matches the SQL fragments the
translators emit, so the two backends the contract binds hardest had no
runtime assertion. Add end-to-end `SQLiteDB` coverage that runs the same
data through both branches of `find` — ORDER BY pushed into SQL, and the
in-memory `finalize_find_results` fallback reached via an unsafe field path —
and asserts they agree on values-then-missing ordering in both directions and
on the true top N under `limit`.

Also normalize a falsy `sort` to `None` in `SQLiteDB.find`. `sort=[]` failed
the `sort is None` guard and took the untranslatable-sort branch, loading the
whole collection and applying `limit` in memory. Same rows, needless work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…transport

Follow-up to the strict-scheduling change, closing the gaps found in review.

Blocker: `dispatch_deferred_task` forwarded `strict=` to `sched.schedule()`
unconditionally. `TaskScheduler` is a public/stable extension point and
`config.task_scheduler` is duck-typed, so every third-party scheduler written
against the pre-`strict` signature would have raised `TypeError` on *every*
dispatch, non-strict included. The signature is now introspected (cached per
class) and `strict` omitted for schedulers that predate it; a `strict=True`
dispatch through one raises `TaskSchedulerNotConfiguredError` explaining it
cannot honor the guarantee.

Remaining silent-failure paths, all now raising under strict:

* `NoopOrSyncScheduler` with no executor — what `get_task_scheduler` returns
  for every non-serverless caller — accepted `strict=True` and dropped the
  task.
* Lambda `invoke` responses were never inspected. An async invoke answers 202
  on acceptance, so a non-2xx `StatusCode` or a `FunctionError` was reported
  as success because boto3 had not raised.
* An EventBridge scheduling failure fell back to invoking immediately with
  `process_at` in the body. Past Lambda's 900s timeout the handler cannot
  survive until `run_at`, so the task was doomed and the caller still got a
  success reference.
* SQS `send_message` failures propagated while the Lambda transport swallowed
  them, so the same application code had opposite failure semantics depending
  on `JVSPATIAL_AWS_DEFERRED_TRANSPORT`. `strict` is now the single switch on
  both.

Replace the ad-hoc `RuntimeError`s with `DeferredTaskError` →
`TaskDispatchError` → `TaskSchedulerNotConfiguredError`, so a strict caller
can distinguish "retry may succeed" from "this deployment will never
dispatch". `DeferredTaskError` also derives from `RuntimeError`, keeping
existing handlers working.

Emit the one-time no-op diagnostic before the strict raise — a deployment
whose callers are all strict never saw the startup error explaining why
nothing dispatched.

Add coverage for SQS, the logging no-op, the sync fallback, `strict`
forwarding through the factory, legacy and `**kwargs` schedulers, and
non-raising invoke rejections; the suite was Lambda-only. Rewrite the
downstream-consumer narration in docstrings and test data generically per
CLAUDE.md, and update `docs/md/serverless-mode.md` and SPEC §11.3, which
still described the old narrow meaning of `strict`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pip-audit job was failing on PYSEC-2026-3447 in setuptools 79.0.1 —
the version bundled with the runner's Python, which lands in the freeze
list the audit consumes. setuptools is neither a declared runtime
dependency of jvspatial nor vendored into its wheel, so the finding said
nothing about the project's own dependency hygiene while still reddening
every PR that touches pyproject.toml or this workflow.

Upgrade setuptools alongside pip so the audited environment carries the
fixed 83.0.0.

Not floored in pyproject's [build-system] requires instead: setuptools 83
requires Python >=3.10 while jvspatial supports >=3.9, so pinning it there
would make a source build unresolvable on 3.9. This job pins 3.11, so the
upgrade is safe here and keeps the audit reporting on our dependencies.

Verified in a clean 3.11 venv: pip-audit --strict reports "No known
vulnerabilities found" after the upgrade.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ns/setup-python-7

ci(deps): bump actions/setup-python from 6 to 7
eldonm and others added 24 commits September 18, 2026 22:30
feat: add Object.create_if_absent atomic insert
feat(db): PostgresTransaction.find_one_and_update for work-kernel CAS
async_transaction_context opened a backend transaction but kept saving through the pool, so Node.create and connect committed immediately. graph_transaction now uses the transaction handle as the GraphContext database and as the task-local default.
Bind Node writes to one graph transaction
Create a GitHub Release when a new version tag is cut
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 24 to 25.
- [Release notes](https://github.com/dawidd6/action-download-artifact/releases)
- [Commits](dawidd6/action-download-artifact@v24...v25)

---
updated-dependencies:
- dependency-name: dawidd6/action-download-artifact
  dependency-version: '25'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(release): prepare jvspatial 0.1.1

* fix(postgres): serialize bootstrap across instances and bound generated names
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 25 to 27.
- [Release notes](https://github.com/dawidd6/action-download-artifact/releases)
- [Commits](dawidd6/action-download-artifact@v25...v27)

---
updated-dependencies:
- dependency-name: dawidd6/action-download-artifact
  dependency-version: '27'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot @github

dependabot Bot commented on behalf of github Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Labels

The following labels could not be found: ci, dependencies. Please create them before Dependabot can add them to a pull request.

Please fix the above issues or remove invalid values from dependabot.yml.

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

Benchmark comparison

Threshold: ±25% (informational, does not block merge)

benchmark baseline (s) current (s) delta status
tests/benchmarks/test_deferred_save_benchmarks.py::test_bench_deferred_save_batched_100 0.027088 0.020841 -23.1% OK
tests/benchmarks/test_deferred_save_benchmarks.py::test_bench_immediate_save_100 0.026505 0.020810 -21.5% OK
tests/benchmarks/test_jsondb_benchmarks.py::test_bench_jsondb_batched_saves_500 0.682750 0.731892 +7.2% OK
tests/benchmarks/test_jsondb_benchmarks.py::test_bench_jsondb_count_empty_query 1.278299 1.542240 +20.6% OK
tests/benchmarks/test_jsondb_benchmarks.py::test_bench_jsondb_count_filtered 1.320848 1.185102 -10.3% OK
tests/benchmarks/test_jsondb_benchmarks.py::test_bench_jsondb_find_filtered 1.132295 0.922768 -18.5% OK
tests/benchmarks/test_jsondb_benchmarks.py::test_bench_jsondb_save_throughput 0.002089 0.002119 +1.4% OK
tests/benchmarks/test_sqlite_benchmarks.py::test_bench_sqlite_count_empty 0.326228 0.238983 -26.7% IMPROVED (-26.7%)
tests/benchmarks/test_sqlite_benchmarks.py::test_bench_sqlite_count_fallback_via_regex 0.435842 0.312030 -28.4% IMPROVED (-28.4%)
tests/benchmarks/test_sqlite_benchmarks.py::test_bench_sqlite_count_pushdown 0.345287 0.239258 -30.7% IMPROVED (-30.7%)
tests/benchmarks/test_sqlite_benchmarks.py::test_bench_sqlite_find_fallback_via_regex 0.442092 0.303389 -31.4% IMPROVED (-31.4%)
tests/benchmarks/test_sqlite_benchmarks.py::test_bench_sqlite_find_pushdown 0.342137 0.256455 -25.0% IMPROVED (-25.0%)
tests/benchmarks/test_sqlite_benchmarks.py::test_bench_sqlite_sort_limit_pushdown 0.374395 0.255066 -31.9% IMPROVED (-31.9%)

@eldonm
eldonm changed the base branch from main to dev October 11, 2026 08:59
@eldonm
eldonm merged commit 62cec92 into dev Oct 11, 2026
6 checks passed
@dependabot
dependabot Bot deleted the dependabot/github_actions/dawidd6/action-download-artifact-27 branch October 11, 2026 09:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants